//(时间获取客户/服务器IPv4协议相关程序：对于时间获取服务，端口号一般为13。由服务器关闭连接表征记录的结束，HTTP 1.0版本也采用这种技术。)
#include <errno.h>
#include <fcntl.h>
#include <signal.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <arpa/inet.h>
#include <sys/types.h>
#include <sys/socket.h>

#define MAXLINE 32
#define error_exit(msg) \
	do { perror(msg); exit(EXIT_FAILURE); } while (0)

int connect_nonb(int sockfd, const struct sockaddr *saptr, socklen_t salen, int nsec) {
	int				flags, n, error;
	socklen_t		len;
	fd_set			rset, wset;
	struct timeval	tval;

	/* 设置套接字为非阻塞 */
	flags = fcntl(sockfd, F_GETFL, 0);
	fcntl(sockfd, F_SETFL, flags | O_NONBLOCK);

	error = 0;
	if ((n = connect(sockfd, saptr, salen)) < 0)
		if (errno != EINPROGRESS) // EINPROGRESS错误表示连接建立已经启动但是尚未完成
			return -1;
	if (n == 0) // 非阻塞connect连接建立完成，通常发生在服务器和客户在同一个主机上的情况
		goto done;

	/* 调用select判断connect连接是否完成建立。关于此有一下说明：
	 * 1.当连接成功建立时，描述符变为可写；
	 * 2.当连接建立遇到错误时，描述符变为既可读又可写；
	 * 3.select调用之前有可能连接已经建立并有对端数据到达，描述符也变为既可读又可写。*/
	FD_ZERO(&rset);
	FD_SET(sockfd, &rset);
	wset = rset;
	tval.tv_sec = nsec;
	tval.tv_usec = 0;
	if ((n = select(sockfd+1, &rset, &wset, NULL, nsec ? &tval : NULL)) == 0) { // select返回0，表示超时发生
		close(sockfd);	
		errno = ETIMEDOUT;
		return -1;
	}
	if (FD_ISSET(sockfd, &rset) || FD_ISSET(sockfd, &wset)) {
		len = sizeof(error);
		if (getsockopt(sockfd, SOL_SOCKET, SO_ERROR, &error, &len) < 0) // 检查套接字上是否存在待处理错误，如果连接成功建立，返回0
			return -1;
	} else
		error_exit("select error: sockfd not set");

done:
	fcntl(sockfd, F_SETFL, flags); // 恢复套接字的文件状态标志

	if (error) {
		close(sockfd);		/* just in case */
		errno = error;
		return -1;
	}
	return 0;
}	

void* Signal(int signo, void (*func)(int)) {
	struct sigaction act, oact;

	act.sa_handler = func; 
	sigemptyset(&act.sa_mask); 
	act.sa_flags = 0;
#ifdef SA_INTERRUPT	
	if (signo == SIGALRM) act.sa_flags |= SA_INTERRUPT;
#endif
#ifdef SA_RESTART 
	if (signo != SIGALRM) act.sa_flags |= SA_RESTART; 
#endif
	if (sigaction(signo, &act, &oact) < 0) 
		return SIG_ERR;
	return oact.sa_handler; // 返回信号的旧行为
}		

static void sig_alarm(int signo) {
	return;	/* just interrupt the connect() */
}	
	
int connect_timeo(int sockfd, const struct sockaddr *saptr, socklen_t salen, int nsec) {
	void *sigfunc;
	int n;

	sigfunc = Signal(SIGALRM, sig_alarm);
	/* 设置报警时钟。如果此前本进程已设置过，alarm返回当前剩余秒数，否则返回0 */
	if (alarm(nsec) != 0)
		printf("connect_timeo: alarm was already set");

	/* 调用connect。如果被SIGALRM中断返回EINTR，把errno改设为ETIMEDOUT */
	if ((n = connect(sockfd, saptr, salen)) < 0) {
		close(sockfd); // 超时关闭套接字，以防三路握手继续进行
		if (errno == EINTR)
			errno = ETIMEDOUT;
	}
	
	/* 关闭alarm并恢复原来的信号处理函数 */
	alarm(0);
	Signal(SIGALRM, sigfunc);

	return n;
}
	
int main(int argc, char **argv) {
	int sockfd, n;
	char recvline[MAXLINE + 1];
	struct sockaddr_in servaddr; // 网际套接字地址结构

	if (argc != 2) 
		error_exit("usage: a.out <IPaddress>");
	
	// 创建网际(AF_INET)字节流(SOCK_STREAM)套接字(TCP)
	if ((sockfd = socket(AF_INET, SOCK_STREAM, 0)) < 0) 
		error_exit("socket");
	
	// 建立连接：connect参数2强制转换成指向"通用套接字地址结构"的指针（开发此函数时，void*还不可用）	
	bzero(&servaddr, sizeof(servaddr));
	servaddr.sin_family = AF_INET; // 地址族
	servaddr.sin_port = htons(13); // 端口号（主机字节序到网络字节序）
	if (inet_pton(AF_INET, argv[1], &servaddr.sin_addr) <= 0) // IP地址（呈现形式到数值）
		error_exit("inet_pton");
	//if (connect(sockfd, (struct sockaddr *)&servaddr, sizeof(servaddr)) < 0)
	//if (connect_nonb(sockfd, (struct sockaddr *)&servaddr, sizeof(servaddr), 3) < 0)
	if (connect_timeo(sockfd, (struct sockaddr *)&servaddr, sizeof(servaddr), 3) < 0) 
		error_exit("connect");

	/* read函数读取服务器的应答，并用fputs输出结果“Thu Jan 12 10:45:52 2017\r\n”(26个字符)。
	 * 因为TCP是一个没有记录边界的字节流协议，这26个字节可以有多种返回方式（包含26个字节的单个TCP分节、每个分节只含1个字节的26个分节等）。
	 * 如果数据量很大，就不能确保一次read调用能返回服务器的整个应答，因此总把read编写在某个循环中。 */
	while ((n = read(sockfd, recvline, MAXLINE)) > 0) { // read()返回0：表明对端关闭连接
		usleep(100);
		recvline[n] = 0;
		if (fputs(recvline, stdout) == EOF) 
			error_exit("fputs");
	}
	if (n < 0) // read()返回负值：表明发生错误
		error_exit("read");
	
	exit(0); // Unix在一个进程终止时总是关闭该进程所有打开的描述符（TCP套接字就此被关闭）
}
